﻿using System;
using System.Collections.Generic;
using System.Linq;
using Cpuss.Properties;

namespace Cpuss
{
    /// <summary>
    /// Runner for CPU scheduling strategies.
    /// </summary>
    /// <remarks>
    /// Runner emulates an enviroment for executing some process load using a defined scheduling strategy. Metrics 
    /// are gathered throughout the simulation for further analysis.
    /// </remarks>
    public sealed class Runner
    {
        private static int _processId = 1;
        private readonly Guid _id;
        private readonly Dictionary<int, Process> _metrics;
        private readonly ProcessLoad _processLoad;
        private readonly IStrategy _strategy;
        private bool _simulationCompleted;
        private int _windowCompletions;
        private List<int> _throughputWindows;
        private int _windowTimeframe;
        private int _windowUpperBound;
        private Process previousProcess;

        /// <summary>
        /// Creates a new instance of <see cref="Runner"/> with a specified <see cref="IStrategy"/>.
        /// </summary>
        /// <param name="strategy">Strategy to use when selecting the next ready process to utilize the CPU.</param>
        /// <exception cref="ArgumentNullException"><strong>strategy</strong> is <strong>null</strong>.</exception>
        private Runner(IStrategy strategy)
        {
            if (strategy == null)
            {
                throw new ArgumentNullException("strategy", Resources.RunnerStrategyNull);
            }

            _strategy = strategy;
            _metrics = new Dictionary<int, Process>();
            _id = Guid.NewGuid();
        }

        /// <summary>
        /// Creates a new instance of <see cref="Runner"/> using a defined <see cref="List{Process}"/> and <see cref="IStrategy"/>.
        /// </summary>
        /// <param name="processLoad">Processes to be used in the simulation.</param>
        /// <param name="strategy">Strategy to use when selecting the next ready process to utilize the CPU.</param>
        /// <exception cref="ArgumentNullException"><strong>processLoad</strong> or <strong>strategy</strong> is <strong>null</strong>.</exception>
        /// <exception cref="ArgumentException"><strong>processLoad</strong> contains <strong>0</strong> <see cref="Process"/>'s.</exception>
        public Runner(ProcessLoad processLoad, IStrategy strategy)
            : this(strategy)
        {
            if (processLoad == null)
            {
                throw new ArgumentNullException("processLoad");
            }
            else if (processLoad.Count < 1)
            {
                throw new ArgumentException(Resources.ProcessLoadEmpty);
            }

            _processLoad = processLoad;
            WireProcessEvents();
            ThroughputSetup(_windowTimeframe);
        }

        /// <summary>
        /// Creates a new instance of <see cref="Runner"/> using a defined <see cref="List{Process}"/>, <see cref="IStrategy"/> and throughput
        /// timeframe.
        /// </summary>
        /// <param name="processLoad">Processes to be used in the simulation.</param>
        /// <param name="strategy">Strategy to use when selecting the next ready process to utilize the CPU.</param>
        /// <param name="windowTimeframe">Window timeframe to use for throughput.</param>
        /// <exception cref="ArgumentOutOfRangeException"><strong>windowTimeframe</strong> is less than <strong>0</strong>.</exception>
        public Runner(ProcessLoad processLoad, IStrategy strategy, int windowTimeframe)
            : this(processLoad, strategy)
        {
            if (windowTimeframe < 0)
            {
                throw new ArgumentOutOfRangeException(Resources.WindowTimeFrameLessThanZero);
            }
            _windowTimeframe = windowTimeframe;
            ThroughputSetup(_windowTimeframe);
        }


        /// <summary>
        /// Creates a new instance of <see cref="Runner"/> using a defined number of small, medium, and large processes and
        /// <see cref="IStrategy"/>.
        /// </summary>
        /// <remarks>
        /// Burst times ranges are defined in <see cref="BurstTime"/>.
        /// Each process has the following generated:  <see cref="Process.ArrivalTime"/>, <see cref="Process.Priority"/> and unique <see cref="Process.Id"/>.
        /// </remarks>
        /// <param name="small">Number of small processes.</param>
        /// <param name="medium">Number of medium processes.</param>
        /// <param name="large">Number of large processes.</param>
        /// <param name="strategy">Strategy to use when selecting the next ready process to utilize the CPU.</param>
        /// <exception cref="ArgumentException"><strong>0</strong> processes are specified for the simulation.</exception>
        /// <exception cref="ArgumentNullException"><strong>strategy</strong> is <strong>null</strong>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><strong>small</strong>, <strong>medium</strong>, or <strong>large</strong>
        /// is less than <strong>zero</strong>.</exception>
        public Runner(int small, int medium, int large, IStrategy strategy)
            : this(strategy)
        {
            if ((small + medium + large) < 1)
            {
                throw new ArgumentException(Resources.NoProcessesToRun);
            }
            else if (small < 0)
            {
                throw new ArgumentOutOfRangeException("small");
            }
            else if (medium < 0)
            {
                throw new ArgumentOutOfRangeException("medium");
            }
            else if (large < 0)
            {
                throw new ArgumentOutOfRangeException("large");
            }

            _processLoad = new ProcessLoad();
            var random = new Random();
            int total = small + medium + large; // used as the upper limit when generating the ArrivalTime of a process
            // generate the processes to satisfy the properties defined by the user.
            for (int i = 0; i < small; i++)
            {
                _processLoad.Add(new Process(_processId++,
                                             random.Next((int) BurstTime.SmallMin, (int) BurstTime.SmallMax),
                                             random.Next(0, total))
                                     {
                                         Priority = (Priority) random.Next(0, 3)
                                     });
            }
            for (int i = 0; i < medium; i++)
            {
                _processLoad.Add(new Process(_processId++,
                                             random.Next((int) BurstTime.MediumMin, (int) BurstTime.MediumMax),
                                             random.Next(0, total))
                                     {
                                         Priority = (Priority) random.Next(0, 3)
                                     });
            }
            for (int i = 0; i < large; i++)
            {
                _processLoad.Add(new Process(_processId++,
                                             random.Next((int) BurstTime.LargeMin, (int) BurstTime.LargeMax),
                                             random.Next(0, total))
                                     {
                                         Priority = (Priority) random.Next(0, 3)
                                     });
            }

            WireProcessEvents();
            ThroughputSetup(_windowTimeframe);
        }

        /// <summary>
        /// Creates a new instance of <see cref="Runner"/> using a defined number of small, medium, and large processes and
        /// <see cref="IStrategy"/> with a defined throughput timeframe.
        /// </summary>
        /// <param name="small">Number of small processes.</param>
        /// <param name="medium">Number of medium processes.</param>
        /// <param name="large">Number of large processes.</param>
        /// <param name="strategy">Strategy to use when selecting the next ready process to utilize the CPU.</param>
        /// <param name="windowTimeframe">Window timeframe to use for throughput.</param>
        /// <exception cref="ArgumentOutOfRangeException"><strong>windowTimeframe</strong> is less than <strong>1</strong>.</exception>
        public Runner(int small, int medium, int large, IStrategy strategy, int windowTimeframe)
            : this(small, medium, large, strategy)
        {
            if (windowTimeframe < 0)
            {
                throw new ArgumentOutOfRangeException(Resources.WindowTimeFrameLessThanZero);
            }
            _windowTimeframe = windowTimeframe;
            ThroughputSetup(_windowTimeframe);
        }


        /// <summary>
        /// Gets the current time the simulation has been running for.
        /// </summary>
        public int Time { get; private set; }

        /// <summary>
        /// Gets the total idle CPU time of the simulation.
        /// </summary>
        public int IdleCpuTime { get; private set; }

        /// <summary>
        /// Gets the total busy CPU time of the simulation.
        /// </summary>
        /// 
        public int BusyCpuTime
        {
            get { return Time - IdleCpuTime; }
        }

        /// <summary>
        /// Gets the process load being used for the simulation.
        /// </summary>
        public ProcessLoad ProcessLoad
        {
            get { return _processLoad; }
        }

        /// <summary>
        /// Gets the metrics for a simulation run.
        /// </summary>
        public Dictionary<int, Process> Metrics
        {
            get { return _metrics; }
        }

        /// <summary>
        /// Gets the Id of the <see cref="Runner"/>.
        /// </summary>
        public Guid Id
        {
            get { return _id; }
        }

        /// <summary>
        /// Gets the strategy being used by the <see cref="Runner"/>.
        /// </summary>
        public string Strategy
        {
            get { return _strategy.ToString(); }
        }

        /// <summary>
        /// Gets the window time frame in ns used for the throughput window.
        /// </summary>
        public int WindowTimeframe
        {
            get { return _windowTimeframe; }
        }

        /// <summary>
        /// Occurs when a <see cref="Process"/> in <see cref="Runner.ProcessLoad"/> utilizes the CPU for the first time.
        /// </summary>
        public event EventHandler<ProcessStartedEventArgs> ProcessStarted;

        /// <summary>
        /// Occurs when a <see cref="Process"/> in <see cref="Runner.ProcessLoad"/> resumes utilization of the CPU.
        /// </summary>
        public event EventHandler<ProcessResumedEventArgs> ProcessResumed;

        /// <summary>
        /// Occurs when a <see cref="Process"/> in <see cref="Runner.ProcessLoad"/> has utilized enough CPU time to voluntarily release the CPU.
        /// </summary>
        public event EventHandler<ProcessCompletedEventArgs> ProcessCompleted;

        /// <summary>
        /// Occurs when a <see cref="Process"/> in <see cref="Runner.ProcessLoad"/> is preempted by another <see cref="Process"/>.
        /// </summary>
        public event EventHandler<ProcessPreemptedEventArgs> ProcessPreempted;

        /// <summary>
        /// Occurs when the <see cref="Runner"/> starts.
        /// </summary>
        public event EventHandler<RunnerStartedEventArgs> Started;

        /// <summary>
        /// Occurs when the <see cref="Runner"/> completes.
        /// </summary>
        public event EventHandler<RunnerCompletedEventArgs> Completed;

        ///<summary>
        /// Gets the % CPU Utilization of the simulation.
        ///</summary>
        ///<returns>% CPU utilization.</returns>
        ///<exception cref="InvalidOperationException">Attempt to get the % CPU utilizaton <strong>before</strong>
        /// the simulation has completed.</exception>
        public double CpuUtilization
        {
            get
            {
                if (!_simulationCompleted)
                {
                    throw new InvalidOperationException(Resources.CpuUtilizationBeforeSimulationCompleted);
                }

                return Math.Round(((double)BusyCpuTime / Time) * 100, 2);
            }
        }

        /// <summary>
        /// Gets the windows of throughput, each value in the <see cref="List{T}"/> is the number of processes that completed in that window.
        /// </summary>
        /// <exception cref="InvalidOperationException">ThroughputWindows accessed <strong>before</strong> the simulation has completed.</exception>
        public List<int> ThroughputWindows
        {
            get
            {
                if (!_simulationCompleted)
                {
                    throw new InvalidOperationException(Resources.ThroughputWindowsBeforeCompletion);
                }
                return _throughputWindows;
            }
        }

        /// <summary>
        /// Initializes the data structures required for gathering throughput metrics.
        /// </summary>
        private void ThroughputSetup(int windowTimeframe)
        {
            if (windowTimeframe < 1)
            {
                _windowTimeframe = 4*((int) Math.Floor(GetBurstTimeMean()));
            }
            _windowUpperBound = Time + _windowTimeframe;
            _throughputWindows = new List<int>();
            _windowCompletions = 0;
        }

        /// <summary>
        /// Wires the <see cref="Process.Started"/> and <see cref="Process.Completed"/> events for each <see cref="Process"/> in <see cref="ProcessLoad"/>
        /// to the correct handler method.
        /// </summary>
        private void WireProcessEvents()
        {
            foreach (Process process in _processLoad)
            {
                process.Started += Process_Started;
                process.Completed += Process_Completed;
            }
        }

        /// <summary>
        /// Raises <see cref="ProcessStarted"/> event.
        /// </summary>
        /// <param name="e">Data associated with a <see cref="Process"/> when started.</param>
        private void OnProcessStarted(ProcessStartedEventArgs e)
        {
            EventHandler<ProcessStartedEventArgs> temp = ProcessStarted;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Invoked when a process in the <see cref="ProcessLoad"/> utilizes the CPU for the first time.
        /// </summary>
        /// <param name="sender">Sender of event.</param>
        /// <param name="e">Data associated with the <see cref="Process"/> when started.</param>
        private void Process_Started(object sender, ProcessStartedEventArgs e)
        {
            OnProcessStarted(e);
        }

        /// <summary>
        /// Raises <see cref="ProcessCompleted"/> event.
        /// </summary>
        /// <param name="e">Data associated with a <see cref="Process"/> when completed.</param>
        private void OnProcessCompleted(ProcessCompletedEventArgs e)
        {
            EventHandler<ProcessCompletedEventArgs> temp = ProcessCompleted;
            if (temp != null)
            {
                temp(this, e);
            }
            if (e.CompletionTime <= _windowUpperBound)
            {
                _windowCompletions++;
            }
        }

        /// <summary>
        /// Invoked when a process in the <see cref="ProcessLoad"/> has utilized enough CPU time to voluntarily release
        /// the CPU.
        /// </summary>
        /// <param name="sender">Sender of event.</param>
        /// <param name="e">Data associated with the <see cref="Process"/> when completed.</param>
        private void Process_Completed(object sender, ProcessCompletedEventArgs e)
        {
            OnProcessCompleted(e);
        }

        /// <summary>
        /// Raises <see cref="Started"/> event.
        /// </summary>
        /// <param name="e">Data associated with runner when started.</param>
        private void OnStarted(RunnerStartedEventArgs e)
        {
            EventHandler<RunnerStartedEventArgs> temp = Started;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Raises <see cref="Completed"/> event.
        /// </summary>
        /// <param name="e">Data associated with runner when completed.</param>
        private void OnCompleted(RunnerCompletedEventArgs e)
        {
            EventHandler<RunnerCompletedEventArgs> temp = Completed;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Raises <see cref="ProcessResumed"/> event.
        /// </summary>
        /// <param name="e">Data associated with <see cref="Process"/> when it resumes utilization of the CPU.</param>
        private void OnProcessResumed(ProcessResumedEventArgs e)
        {
            EventHandler<ProcessResumedEventArgs> temp = ProcessResumed;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Raises <see cref="ProcessPreempted"/> event.
        /// </summary>
        /// <param name="e">Data associated with <see cref="Process"/> when preemption occurs.</param>
        private void OnProcessPreempted(ProcessPreemptedEventArgs e)
        {
            EventHandler<ProcessPreemptedEventArgs> temp = ProcessPreempted;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Invokes the simulation.
        /// </summary>
        public void Run()
        {
            OnStarted(new RunnerStartedEventArgs(_processLoad.Count, _strategy));
            _strategy.Execute(this);
            foreach (var process in _metrics)
            {
                process.Value.WaitTime = GetWaitTime(process.Value);
            }
            OnCompleted(new RunnerCompletedEventArgs(_processLoad.Count, _strategy, Time));
            // check to see if a process(s) finished in a time window that spanned past the total simulation time
            if (_windowCompletions != 0)
            {
                _throughputWindows.Add(_windowCompletions);
            }
            _simulationCompleted = true;
        }

        /// <summary>
        /// Logs the metrics of a <see cref="Process"/> to <see cref="Metrics"/> using the <see cref="Process.Id"/> as the key.
        /// </summary>
        /// <param name="process">Process to log the metrics of.</param>
        public void LogProcessMetrics(Process process)
        {
            _metrics[process.Id] = process;
        }

        /// <summary>
        /// Utilizes the CPU for a single ns updating the executing <see cref="Process.Data"/> and <see cref="Runner"/> 
        /// properties accordingly.
        /// </summary>
        /// <param name="process">Process to utilize the CPU.</param>
        public void UtilizeCpu(Process process)
        {
            if (process != previousProcess)
            {
                process.CpuActivity.Add(new Pair<int, int>
                                            {
                                                First = Time
                                            });
                if (IsPreempted(ref previousProcess))
                {
                    OnProcessPreempted(new ProcessPreemptedEventArgs(previousProcess.Id, previousProcess.BurstTime,
                                                                     previousProcess.ArrivalTime,
                                                                     previousProcess.Priority));
                }
                if (IsResuming(ref process))
                {
                    OnProcessResumed(new ProcessResumedEventArgs(process.Id, process.BurstTime, process.ArrivalTime,
                                                                 process.Priority, process.StartTime));
                }
                previousProcess = process;
            }
            process.CpuActivity[process.CpuActivity.Count - 1].Second = Time + 1;
            int utilized = process.Data.UtilizedCpuTime + 1;
            process.Data = new ProcessRunData(utilized, ++Time);
            // add the number of processes to _throughputWindows if we are at the end of the current window bound
            if (Time == _windowUpperBound)
            {
                _throughputWindows.Add(_windowCompletions);
                _windowCompletions = 0;
                _windowUpperBound += _windowTimeframe;
            }
        }

        /// <summary>
        /// Determines whether or not a process has been preempted by another process.
        /// </summary>
        /// <param name="process">Process to check if it has been preempted.</param>
        /// <returns>True if the process has been preempted, false otherwise.</returns>
        private static bool IsPreempted(ref Process process)
        {
            return process != null && process.Data.UtilizedCpuTime > 0 &&
                   process.Data.UtilizedCpuTime < process.BurstTime;
        }

        /// <summary>
        /// Determines whether or not a process is resuming CPU utilization.
        /// </summary>
        /// <param name="process">Process to check if it is resuming CPU utilization.</param>
        /// <returns>True if the process is resuming, false otherwise.</returns>
        private bool IsResuming(ref Process process)
        {
            return process.Data.UtilizedCpuTime > 0 &&
                   (Time - process.CpuActivity[process.CpuActivity.Count - 1].Second) > 0;
        }

        /// <summary>
        /// Utilizes the CPU for a defined duration updating the executing <see cref="Process.Data"/> and <see cref="Runner"/> 
        /// properties accordingly.
        /// </summary>
        /// <param name="process">Process to utilize the CPU.</param>
        /// <param name="duration">Duration to utilize CPU for.</param>
        public void UtilizeCpu(Process process, int duration)
        {
            int elapsed = 0;
            while (elapsed < duration && !Process.IsComplete(process))
            {
                UtilizeCpu(process);
                elapsed++;
            }
        }

        /// <summary>
        /// Skips a single nanosecond (ns) of idle CPU time.
        /// </summary>
        public void SkipIdleCpuTime()
        {
            IdleCpuTime++;
            Time++;
        }

        /// <summary>
        /// Skips all idle CPU time up until the arrival time of a given process.
        /// </summary>
        /// <param name="process">Process to skip idle time until.</param>
        public void SkipIdleCpuTime(Process process)
        {
            while (Time < process.ArrivalTime)
            {
                IdleCpuTime++;
                Time++;
            }
        }

        /// <summary>
        /// Gets the wait time for a process.
        /// </summary>
        /// <param name="process">Process to get wait time for.</param>
        /// <returns>Wait time of process.</returns>
        private static int GetWaitTime(Process process)
        {
            int totalWaitTime = 0;
            if (process.CpuActivity.Count > 1)
            {
                int i = process.CpuActivity.Count - 1;
                while (i > 0)
                {
                    totalWaitTime += process.CpuActivity[i].First - process.CpuActivity[i - 1].Second;
                    i--;
                }
            }
            totalWaitTime += process.StartTime - process.ArrivalTime;
            return totalWaitTime;
        }

        /// <summary>
        /// Gets the average wait time for all processes executed in the simulation.
        /// </summary>
        /// <returns>Average wait time of all processes (ns).</returns>
        /// <exception cref="InvalidOperationException">Mean wait time calculated <strong>before</strong> the simulation has been invoked.</exception>
        public double GetWaitTimeMean()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetWaitTimeMeanInvokedBeforeSimulationCompleted);
            }

            return _processLoad.Select(x => x.WaitTime).Mean();
        }

        /// <summary>
        /// Gets the average wait time for all the processes that satisfy the specified predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Average wait time of all processes that satisfy the predicate function.</returns>
        /// <exception cref="InvalidOperationException">Mean wait time calculated <strong>before</strong> the simulation has been invoked.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetWaitTimeMean(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetWaitTimeMeanInvokedBeforeSimulationCompleted);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.WaitTime).Mean();
        }

        /// <summary>
        /// Gets the variance for the process wait times.
        /// </summary>
        /// <returns>Variance for process wait times.</returns>
        /// <exception cref="InvalidOperationException">
        /// Variance for process wait times calculated <strong>before</strong> the simulation has been 
        /// invoked.
        /// </exception>
        public double GetWaitTimeVariance()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetVarianceInvokedBeforeSimulationInvoked);
            }

            return _processLoad.Select(x => x.WaitTime).Variance();
        }

        /// <summary>
        /// Gets the variance for the process wait times that satisfy the provided predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Variance for process wait times.</returns>
        /// <exception cref="InvalidOperationException">Variance for process wait times calculated <strong>before</strong> the simulation has been 
        /// invoked.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetWaitTimeVariance(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetVarianceInvokedBeforeSimulationInvoked);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.WaitTime).Variance();
        }

        /// <summary>
        /// Gets standard deviation of process wait times.
        /// </summary>
        /// <returns>Standard deviation of process wait times.</returns>
        /// <exception cref="InvalidOperationException">
        /// Standard deviation for process wait times calculated <strong>before</strong> the simulation has been 
        /// invoked.
        /// </exception>
        public double GetWaitTimeStandardDeviation()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetWaitTimeStandardDeviationBeforeSimulationInvoked);
            }

            return Math.Sqrt(GetWaitTimeVariance());
        }

        /// <summary>
        /// Gets standard deviation of process wait times that satisfy the provided predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Standard deviation of process wait times.</returns>
        /// <exception cref="InvalidOperationException">
        /// Standard deviation for process wait times calculated <strong>before</strong> the simulation has been 
        /// invoked.
        /// </exception>
        public double GetWaitTimeStandardDeviation(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetWaitTimeStandardDeviationBeforeSimulationInvoked);
            }

            return Math.Sqrt(GetWaitTimeVariance(predicate));
        }

        /// <summary>
        /// Gets the minimum wait time experienced by any process.
        /// </summary>
        /// <returns>Minimum wait time.</returns>
        /// <exception cref="InvalidOperationException">Minimum wait time calculated <strong>before</strong> the simulation has been invoked.</exception>
        public int GetMinimumWaitTime()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetMinimumWaitTimeBeforeSimulationInvoked);
            }

            int min = Int32.MaxValue;
            foreach (var process in _metrics)
            {
                if (process.Value.WaitTime == 0)
                {
                    return 0; // can't get any less than that
                }
                else if (process.Value.WaitTime < min)
                {
                    min = process.Value.WaitTime;
                }
            }
            return min;
        }

        /// <summary>
        /// Gets the maximum wait time experienced by any process.
        /// </summary>
        /// <returns>Maximum wait time.</returns>
        /// <exception cref="InvalidOperationException">Maximum wait time calculated <strong>before</strong> the simulation has been invoked.</exception>
        public int GetMaximumWaitTime()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetMaximumWaitTimeBeforeSimulationInvoked);
            }

            int max = 0;
            foreach (var process in _metrics)
            {
                if (process.Value.WaitTime > max)
                {
                    max = process.Value.WaitTime;
                }
            }
            return max;
        }

        /// <summary>
        /// Gets the mean turnaround time for all processes.
        /// </summary>
        /// <returns>Mean turnaround time.</returns>
        /// <exception cref="InvalidOperationException">Turnaround time mean calculated <strong>before</strong> simulation has completed.</exception>
        public double GetTurnaroundTimeMean()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeMeanBeforeSimulationComplete);
            }

            return _processLoad.Select(x => x.TurnaroundTime).Mean();
        }

        /// <summary>
        /// Gets the mean turnaround time for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Mean turnaround time.</returns>
        /// <exception cref="InvalidOperationException">Turnaround time mean calculated <strong>before</strong> simulation has completed.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetTurnaroundTimeMean(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeMeanBeforeSimulationComplete);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.TurnaroundTime).Mean();
        }

        /// <summary>
        /// Gets the mean response time for all processes.
        /// </summary>
        /// <returns>Mean response time.</returns>
        /// <exception cref="InvalidOperationException">Response time mean calculated <strong>before</strong> simulation has completed.</exception>
        public double GetResponseTimeMean()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeMeanBeforeSimulationComplete);
            }

            return _processLoad.Select(x => x.ResponseTime).Mean();
        }

        /// <summary>
        /// Gets the mean response time for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Mean response time.</returns>
        /// <exception cref="InvalidOperationException">Response time mean calculated <strong>before</strong> simulation has completed.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetResponseTimeMean(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeMeanBeforeSimulationComplete);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.ResponseTime).Mean();
        }

        /// <summary>
        /// Gets the mean of all the process burst times.
        /// </summary>
        /// <returns>Mean burst time.</returns>
        public double GetBurstTimeMean()
        {
            return _processLoad.Select(x => x.BurstTime).Mean();
        }

        /// <summary>
        /// Gets the mean throughput for a time window <em>t</em>.
        /// </summary>
        /// <returns>Mean process throughput.</returns>
        /// <exception cref="InvalidOperationException">Throughput mean calculated <strong>before</strong> simulation has completed.</exception>
        public double GetThroughputMean()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetThroughputMeanBeforeSimulationComplete);
            }

            return _throughputWindows.Mean();
        }

        /// <summary>
        /// Gets the response time variance for all processes.
        /// </summary>
        /// <returns>Response time variance.</returns>
        /// <exception cref="InvalidOperationException">Response time variance is calculated <strong>before</strong> the simulation has completed.</exception>
        public double GetResponseTimeVariance()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeVarianceBeforeSimulationCompleted);
            }

            return _processLoad.Select(x => x.ResponseTime).Variance();
        }

        /// <summary>
        /// Gets the response time variance for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Respone time variance.</returns>
        /// <exception cref="InvalidOperationException">Response time variance is calculated <strong>before</strong> the simulation has completed.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetResponseTimeVariance(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeVarianceBeforeSimulationCompleted);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.ResponseTime).Variance();
        }

        /// <summary>
        /// Gets the response time standard deviation for all processes.
        /// </summary>
        /// <returns>Response time standard deviation.</returns>
        /// <exception cref="InvalidOperationException">Response time standard deviation is calculated <strong>before</strong> the simulation 
        /// has completed.</exception>
        public double GetResponseTimeStandardDeviation()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeStandardDeviationBeforeSimulationCompleted);
            }

            return Math.Sqrt(GetResponseTimeVariance());
        }

        /// <summary>
        /// Gets the response time standard deviation for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Response time standard deviation.</returns>
        /// <exception cref="InvalidOperationException">Response time standard deviation is calculated <strong>before</strong> the simulation 
        /// has completed.</exception>
        public double GetResponseTimeStandardDeviation(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetResponseTimeStandardDeviationBeforeSimulationCompleted);
            }

            return Math.Sqrt(GetResponseTimeVariance(predicate));
        }

        /// <summary>
        /// Gets the turnaround time variance for all processes.
        /// </summary>
        /// <returns>Turnaround time variance.</returns>
        /// <exception cref="InvalidOperationException">Turnaround time variance is calculated <strong>before</strong> the simulation has completed.</exception>
        public double GetTurnaroundTimeVariance()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeVarianceBeforeSimulationCompleted);
            }

            return _processLoad.Select(x => x.TurnaroundTime).Variance();
        }

        /// <summary>
        /// Gets the turnaround time variance for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <exception cref="InvalidOperationException">Turnaround time variance is calculated <strong>before</strong> the simulation has completed.</exception>
        /// <exception cref="ArgumentNullException"><strong>predicate</strong> is <strong>null</strong>.</exception>
        public double GetTurnaroundTimeVariance(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeVarianceBeforeSimulationCompleted);
            }
            else if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return _processLoad.Where(predicate).Select(x => x.TurnaroundTime).Variance();
        }

        /// <summary>
        /// Gets the turnaround time standard deviation for all processes.
        /// </summary>
        /// <returns>Turnaround time standard deviation.</returns>
        /// <exception cref="InvalidOperationException">Turnaround time standard deviation is calculated <strong>before</strong> the simulation 
        /// has completed.</exception>
        public double GetTurnaroundTimeStandardDeviation()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeStandardDeviationSimulationCompleted);
            }

            return Math.Sqrt(GetTurnaroundTimeVariance());
        }

        /// <summary>
        /// Gets the turnaround time standard deviation for all processes that satisfy a predicate function.
        /// </summary>
        /// <param name="predicate">Predicate function.</param>
        /// <returns>Turnaround time standard deviation.</returns>
        /// <exception cref="InvalidOperationException">Turnaround time standard deviation is calculated <strong>before</strong> the simulation 
        /// has completed.</exception>
        public double GetTurnaroundTimeStandardDeviation(Func<Process, bool> predicate)
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetTurnaroundTimeStandardDeviationSimulationCompleted);
            }

            return Math.Sqrt(GetTurnaroundTimeVariance(predicate));
        }

        /// <summary>
        /// Gets the throughput variance for a time window <em>t</em>.
        /// </summary>
        /// <returns>Throughput variance.</returns>
        /// <exception cref="InvalidOperationException">Throughput time variance is calculated <strong>before</strong> the simulation 
        /// has completed.</exception>
        public double GetThroughputVariance()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetThroughputVarianceBeforeSimulationComplete);
            }

            return _throughputWindows.Variance();
        }

        /// <summary>
        /// Gets the throughput standard deviation for a time window <em>t</em>.
        /// </summary>
        /// <returns>Throughput standard deviation.</returns>
        public double GetThroughputStandardDeviation()
        {
            if (!_simulationCompleted)
            {
                throw new InvalidOperationException(Resources.GetThroughputStandardDeviationBeforeSimulationComplete);
            }

            return Math.Sqrt(GetThroughputVariance());
        }
    }
}